'''
= Automatic Attributes =

This module extends Python with automatic attribute support. Automatic
attributes are created by inheriting a class from Object (note the capital
'O') and then using the Attribute constructor in the class body:

    class Foo(Object):
        bar = Attribute()
        baz = Attribute()

This will create getter- and setter-properties for the 'bar' and 'baz'
attributes. See the Attribute class documentation for details.
'''

from __future__ import division

from weakref import WeakKeyDictionary

import random


# TODO: Finish documenting attributes.py


class Object(object):
    '''Base type for types with automatic attribute access.
    
    See Attribute type documentation for details.'''
    
    __attributes  = WeakKeyDictionary()
    
    @classmethod
    def attributes(cls):
        '''All automatic attributes for this type.
        
        Note: Cached after first use, adding attributes at run time is not
        currently supported for this method!'''
        
        # TODO: Move to metaclass
        
        if cls not in cls.__attributes:
            entries = [(name, getattr(cls, name))
                       for name in dir(cls)]
            cls.__attributes[cls] = sorted([(attribute._order, name, attribute)
                                            for name, attribute in entries
                                            if isinstance(attribute, Attribute)])
            
            for _, name, attribute in cls.__attributes[cls]:
                attribute.set_slot('_' + name)
        
        return cls.__attributes[cls]
    
    @classmethod
    def members_of_class(cls, member_class):
        '''All members of this class object which are of the given type.'''
        
        for name in dir(cls):
            member = getattr(cls, name)
            
            if isinstance(member, member_class):
                yield member
    
    def __init__(self, *args, **kwds):
        '''Constructor, any arguments allowed.
        
        The arguments will be distributed among the automatic attributes, then
        the setup-method will be called. Attributes that are not initiable or
        which do not have an argument have their default initial value
        assigned.'''
        
        super(Object, self).__init__()
        
        args = list(args)
        
        for _, name, attribute in self.__class__.attributes():
            if attribute._initiable is False:
                value = attribute._initial
            elif args:
                value = args.pop(0)
            elif name in kwds:
                value = kwds[name]
            else:
                value = attribute._initial
            
            if value is not None:
                setattr(self, name, value)
        
        self.__has_changed = False
        self.setup()

    def setup(self):
        '''Template method, called in the constructor after all attributes have been initialized.'''
        pass
    
    @property
    def has_changed(self):
        '''Have attributes we track changed?'''
        
        return self.__has_changed
    
    def changed(self, *args, **kwds):
        '''Notify of an attribute change.'''
        
        self.__has_changed = True
        
        self.on_changed(*args, **kwds)
    
    def clear_changed(self):
        '''Clear the changed flag.'''
        
        self.__has_changed = False
    
    def on_changed(self, *args, **kwds):
        '''Template method, called when a tracked variable changes.'''
        pass
    
    def is_a(self, cls):
        '''Sugar for isinstance(self, cls).'''
        
        return isinstance(self, cls)
    
    is_an = is_a   # Mmmmm, raw sugar, how sweet!
    
    def is_like(self, object):
        '''Are both objects of the same class?'''
        
        return self.__class__ == object.__class__
    
    def one_in(self, number):
        return random.random() < 1 / number
    
    def chance(self, number):
        return random.random() < number
    
    @property
    def type(self):
        return self.__class__


class Attribute(object):
    '''Automatical attribute.
    '''
    
    __order = 0
    
    @classmethod
    def next_order(cls):
        cls.__order += 1
        
        return cls.__order

    def __init__(self, slot = None, initial = None, setter = None, writable = False,
                 convert = None, transform = None, initiable = True, changed = None,
                 clamp = None, track_changes = False):
        self._order         = self.__class__.next_order()
        self._slot          = slot
        self._initial       = initial
        self._setter        = setter
        self._writable      = writable
        self._convert       = convert
        self._transform     = transform
        self._initiable     = initiable
        self._changed       = changed
        self._clamp         = clamp
        self._track_changes = track_changes
        self._simple        = (convert is None and transform is None)
    
    def set_slot(self, slot):
        self._slot = slot
    
    def setter(self, function):
        self._setter = function
        
        return self
    
    def __get__(self, instance, owner):
        if instance is None:
            return self
        
        if self._slot is None:
            owner.attributes() # Force update of slots
        
        if self._slot in instance.__dict__ and self._simple:
            return instance.__dict__.get(self._slot, self._initial)
        
        try:
            value = getattr(instance, self._slot)
            # Don't use hasattr(...), since that one only does the the try..except itself
        except AttributeError:
            value = self._initial
        
        if self._transform is None:
            return value
        else:
            return self._transform(value)
    
    def __set__(self, instance, value):
        original = None
        
        if self._convert is not None:
            value = self._convert(value)
        
        if self._clamp is not None:
            lower, upper = self._clamp
            
            if lower is None:
                lower = min(value, upper)
            
            if upper is None:
                upper = max(value, lower)
            
            if isinstance(lower, Attribute):
                lower = lower.__get__(instance, instance.__class__)
            
            if isinstance(upper, Attribute):
                upper = upper.__get__(instance, instance.__class__)
            
            value = min(max(value, lower), upper)
        
        if self._slot is None:
            instance.__class__.attributes() # Force update of slots
        
        if (not self._writable or self._track_changes) and hasattr(instance, self._slot):
            # Only do hasattr(...) once, as it is fairly expensive
            
            if not self._writable:
                raise AttributeError('Cannot assign to read-only attribute')
        
            if self._track_changes:
                original = getattr(instance, self._slot)
        
        if self._setter is None:
            setattr(instance, self._slot, value)
        else:
            self._setter(instance, value)
        
        if self._changed is not None:
            self._changed(instance)
        
        if self._track_changes and original != value:
            instance.changed()
    
    @property
    def _(self):
        return Delegator(self)


def attribute_type_based_on(type):
    def ctor(initial = type(), convert = type, transform = type, **kwds):
        return Attribute(initial = initial, convert = convert,
                         transform = transform, **kwds)
    
    return ctor


ListAttribute = attribute_type_based_on(list)
DictAttribute = attribute_type_based_on(dict)
SetAttribute  = attribute_type_based_on(set)


class Delegator(object):
    def __init__(self, member):
        self._member = member
    
    @property
    def member(self):
        return self._member
    
    def value(self, instance):
        return self.member.__get__(instance, instance.__class__)
    
    def __getitem__(self, key):
        return property(lambda instance: self.value(instance)[key])
    
    def __getattr__(self, name):
        def get(instance):
            value = self.value(instance)
            
            if value is None:
                return None
            else:
                return getattr(value, name)
        
        return property(get)


class _(object):
    def __init__(self, *args):
        if len(args) == 1:
            x = args[0]
        else:
            x = tuple(args)
        
        self._x              = x
        self._instance_value = None
        self._callable       = (callable(x) and not isinstance(x, type))
    
    def dynamic(self, f):
        self._instance_value = f
        
        return self
    
    def __get__(self, instance, owner):
        if instance is not None and self._instance_value is not None:
            return self._instance_value(instance)
        elif self._callable:
            return self._x(instance)
        else:
            return self._x
    
    def __iter__(self):
        return iter(self._x)


class Plural(object):
    def __init__(self, name):
        self._name = name
    
    def __get__(self, instance, owner):
        if instance is None:
            return ()
        
        return plural(getattr(instance, self._name))


def plural(value): 
        if value is None:
            return ()
        elif hasattr(value, '__iter__'):
            return tuple(iter(value))
        else:
            return (value,)
